1// SuperTux - A Jump'n Run
2// Copyright (C) 2004 Ingo Ruhnke <grumbel@gmail.com>
3// Copyright (C) 2006 Christoph Sommer <christoph.sommer@2006.expires.deltadevelopment.de>
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 "worldmap/worldmap.hpp"
19
20#include <physfs.h>
21
22#include "audio/sound_manager.hpp"
23#include "control/input_manager.hpp"
24#include "gui/menu_manager.hpp"
25#include "object/ambient_light.hpp"
26#include "object/decal.hpp"
27#include "object/display_effect.hpp"
28#include "object/music_object.hpp"
29#include "object/tilemap.hpp"
30#include "physfs/ifile_stream.hpp"
31#include "physfs/physfs_file_system.hpp"
32#include "sprite/sprite.hpp"
33#include "squirrel/squirrel_environment.hpp"
34#include "supertux/d_scope.hpp"
35#include "supertux/debug.hpp"
36#include "supertux/fadetoblack.hpp"
37#include "supertux/game_manager.hpp"
38#include "supertux/game_session.hpp"
39#include "supertux/gameconfig.hpp"
40#include "supertux/level.hpp"
41#include "supertux/menu/menu_storage.hpp"
42#include "supertux/player_status_hud.hpp"
43#include "supertux/resources.hpp"
44#include "supertux/savegame.hpp"
45#include "supertux/screen_manager.hpp"
46#include "supertux/shrinkfade.hpp"
47#include "supertux/tile.hpp"
48#include "supertux/tile_manager.hpp"
49#include "util/file_system.hpp"
50#include "util/reader.hpp"
51#include "util/reader_document.hpp"
52#include "util/reader_mapping.hpp"
53#include "video/compositor.hpp"
54#include "video/video_system.hpp"
55#include "video/video_system.hpp"
56#include "video/viewport.hpp"
57#include "worldmap/camera.hpp"
58#include "worldmap/level_tile.hpp"
59#include "worldmap/spawn_point.hpp"
60#include "worldmap/special_tile.hpp"
61#include "worldmap/sprite_change.hpp"
62#include "worldmap/teleporter.hpp"
63#include "worldmap/tux.hpp"
64#include "worldmap/worldmap_parser.hpp"
65#include "worldmap/worldmap_screen.hpp"
66#include "worldmap/worldmap_state.hpp"
67
68namespace worldmap {
69
70WorldMap::WorldMap(const std::string& filename, Savegame& savegame, const std::string& force_spawnpoint_) :
71 m_squirrel_environment(new SquirrelEnvironment(SquirrelVirtualMachine::current()->get_vm(), "worldmap")),
72 m_camera(new Camera),
73 m_enter_level(false),
74 m_tux(),
75 m_savegame(savegame),
76 m_tileset(nullptr),
77 m_name("<no title>"),
78 m_init_script(),
79 m_passive_message_timer(),
80 m_passive_message(),
81 m_map_filename(),
82 m_levels_path(),
83 m_spawn_points(),
84 m_force_spawnpoint(force_spawnpoint_),
85 m_main_is_default(true),
86 m_initial_fade_tilemap(),
87 m_fade_direction(),
88 m_in_level(false)
89{
90 m_tux = &add<Tux>(this);
91 add<PlayerStatusHUD>(m_savegame.get_player_status());
92
93 SoundManager::current()->preload("sounds/warp.wav");
94
95 BIND_WORLDMAP(*this);
96
97 // load worldmap objects
98 WorldMapParser parser(*this);
99 parser.load_worldmap(filename);
100}
101
102WorldMap::~WorldMap()
103{
104 clear_objects();
105 m_spawn_points.clear();
106}
107
108void
109WorldMap::finish_construction()
110{
111 if (!get_object_by_type<AmbientLight>()) {
112 add<AmbientLight>(Color::WHITE);
113 }
114
115 if (!get_object_by_type<MusicObject>()) {
116 add<MusicObject>();
117 }
118
119 if (!get_object_by_type<DisplayEffect>()) {
120 add<DisplayEffect>("Effect");
121 }
122
123 flush_game_objects();
124}
125
126bool
127WorldMap::before_object_add(GameObject& object)
128{
129 m_squirrel_environment->try_expose(object);
130 return true;
131}
132
133void
134WorldMap::before_object_remove(GameObject& object)
135{
136 m_squirrel_environment->try_unexpose(object);
137}
138
139void
140WorldMap::move_to_spawnpoint(const std::string& spawnpoint, bool pan, bool main_as_default)
141{
142 auto sp = get_spawnpoint_by_name(spawnpoint);
143 if (sp != nullptr) {
144 Vector p = sp->get_pos();
145 m_tux->set_tile_pos(p);
146 m_tux->set_direction(sp->get_auto_dir());
147 if (pan) {
148 m_camera->pan();
149 }
150 return;
151 }
152
153 log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl;
154 if (spawnpoint != "main" && main_as_default) {
155 move_to_spawnpoint("main");
156 }
157}
158
159void
160WorldMap::change(const std::string& filename, const std::string& force_spawnpoint_)
161{
162 m_savegame.get_player_status().last_worldmap = filename;
163 ScreenManager::current()->pop_screen();
164 ScreenManager::current()->push_screen(std::make_unique<WorldMapScreen>(
165 std::make_unique<WorldMap>(filename, m_savegame, force_spawnpoint_)));
166}
167
168void
169WorldMap::on_escape_press()
170{
171 // Show or hide the menu
172 if (!MenuManager::instance().is_active()) {
173 MenuManager::instance().set_menu(MenuStorage::WORLDMAP_MENU);
174 m_tux->set_direction(Direction::NONE); // stop tux movement when menu is called
175 } else {
176 MenuManager::instance().clear_menu_stack();
177 }
178}
179
180Vector
181WorldMap::get_next_tile(const Vector& pos, const Direction& direction) const
182{
183 auto position = pos;
184 switch (direction) {
185 case Direction::WEST:
186 position.x -= 1;
187 break;
188 case Direction::EAST:
189 position.x += 1;
190 break;
191 case Direction::NORTH:
192 position.y -= 1;
193 break;
194 case Direction::SOUTH:
195 position.y += 1;
196 break;
197 case Direction::NONE:
198 break;
199 }
200 return position;
201}
202
203bool
204WorldMap::path_ok(const Direction& direction, const Vector& old_pos, Vector* new_pos) const
205{
206 *new_pos = get_next_tile(old_pos, direction);
207
208 if (!(new_pos->x >= 0 && new_pos->x < get_tiles_width()
209 && new_pos->y >= 0 && new_pos->y < get_tiles_height()))
210 { // New position is outsite the tilemap
211 return false;
212 }
213 else
214 { // Check if the tile allows us to go to new_pos
215 int old_tile_data = tile_data_at(old_pos);
216 int new_tile_data = tile_data_at(*new_pos);
217 switch (direction)
218 {
219 case Direction::WEST:
220 return (old_tile_data & Tile::WORLDMAP_WEST
221 && new_tile_data & Tile::WORLDMAP_EAST);
222
223 case Direction::EAST:
224 return (old_tile_data & Tile::WORLDMAP_EAST
225 && new_tile_data & Tile::WORLDMAP_WEST);
226
227 case Direction::NORTH:
228 return (old_tile_data & Tile::WORLDMAP_NORTH
229 && new_tile_data & Tile::WORLDMAP_SOUTH);
230
231 case Direction::SOUTH:
232 return (old_tile_data & Tile::WORLDMAP_SOUTH
233 && new_tile_data & Tile::WORLDMAP_NORTH);
234
235 case Direction::NONE:
236 log_warning << "path_ok() can't walk if direction is NONE" << std::endl;
237 assert(false);
238 }
239 return false;
240 }
241}
242
243void
244WorldMap::finished_level(Level* gamelevel)
245{
246 // TODO use Level* parameter here?
247 auto level = at_level();
248
249 if (level == nullptr) {
250 return;
251 }
252
253 bool old_level_state = level->is_solved();
254 level->set_solved(true);
255
256 // deal with statistics
257 level->get_statistics().update(gamelevel->m_stats);
258
259 if (level->get_statistics().completed(level->get_statistics(), level->get_target_time())) {
260 level->set_perfect(true);
261 }
262
263 save_state();
264
265 if (old_level_state != level->is_solved()) {
266 // Try to detect the next direction to which we should walk
267 // FIXME: Mostly a hack
268 Direction dir = Direction::NONE;
269
270 int dirdata = available_directions_at(m_tux->get_tile_pos());
271 // first, test for crossroads
272 if (dirdata == Tile::WORLDMAP_CNSE ||
273 dirdata == Tile::WORLDMAP_CNSW ||
274 dirdata == Tile::WORLDMAP_CNEW ||
275 dirdata == Tile::WORLDMAP_CSEW ||
276 dirdata == Tile::WORLDMAP_CNSEW)
277 dir = Direction::NONE;
278 else if (dirdata & Tile::WORLDMAP_NORTH
279 && m_tux->m_back_direction != Direction::NORTH)
280 dir = Direction::NORTH;
281 else if (dirdata & Tile::WORLDMAP_SOUTH
282 && m_tux->m_back_direction != Direction::SOUTH)
283 dir = Direction::SOUTH;
284 else if (dirdata & Tile::WORLDMAP_EAST
285 && m_tux->m_back_direction != Direction::EAST)
286 dir = Direction::EAST;
287 else if (dirdata & Tile::WORLDMAP_WEST
288 && m_tux->m_back_direction != Direction::WEST)
289 dir = Direction::WEST;
290
291 if (dir != Direction::NONE) {
292 m_tux->set_direction(dir);
293 }
294 }
295
296 if (!level->get_extro_script().empty()) {
297 try {
298 run_script(level->get_extro_script(), "worldmap:extro_script");
299 } catch(std::exception& e) {
300 log_warning << "Couldn't run level-extro-script: " << e.what() << std::endl;
301 }
302 }
303}
304
305void
306WorldMap::process_input(const Controller& controller)
307{
308 m_enter_level = false;
309
310 if (controller.pressed(Control::ACTION) ||
311 controller.pressed(Control::JUMP) ||
312 controller.pressed(Control::MENU_SELECT))
313 {
314 // some people define UP and JUMP on the same key...
315 if (!controller.pressed(Control::UP)) {
316 m_enter_level = true;
317 }
318 }
319
320 if (controller.pressed(Control::START) ||
321 controller.pressed(Control::ESCAPE))
322 {
323 on_escape_press();
324 }
325
326 if (controller.pressed(Control::CHEAT_MENU) &&
327 g_config->developer_mode)
328 {
329 MenuManager::instance().set_menu(MenuStorage::WORLDMAP_CHEAT_MENU);
330 }
331
332 if (controller.pressed(Control::DEBUG_MENU) &&
333 g_config->developer_mode)
334 {
335 MenuManager::instance().set_menu(MenuStorage::DEBUG_MENU);
336 }
337}
338
339void
340WorldMap::update(float dt_sec)
341{
342 BIND_WORLDMAP(*this);
343
344 if (m_in_level) return;
345 if (MenuManager::instance().is_active()) return;
346
347 GameObjectManager::update(dt_sec);
348
349 m_camera->update(dt_sec);
350
351 {
352 // check for teleporters
353 auto teleporter = at_teleporter(m_tux->get_tile_pos());
354 if (teleporter && (teleporter->is_automatic() || (m_enter_level && (!m_tux->is_moving())))) {
355 m_enter_level = false;
356 if (!teleporter->get_worldmap().empty()) {
357 change(teleporter->get_worldmap(), teleporter->get_spawnpoint());
358 } else {
359 // TODO: an animation, camera scrolling or a fading would be a nice touch
360 SoundManager::current()->play("sounds/warp.wav");
361 m_tux->m_back_direction = Direction::NONE;
362 move_to_spawnpoint(teleporter->get_spawnpoint(), true);
363 }
364 }
365 }
366
367 {
368 // check for auto-play levels
369 auto level = at_level();
370 if (level && level->is_auto_play() && !level->is_solved() && !m_tux->is_moving()) {
371 m_enter_level = true;
372 // automatically mark these levels as solved in case player aborts
373 level->set_solved(true);
374 }
375 }
376
377 {
378 if (m_enter_level && !m_tux->is_moving())
379 {
380 /* Check level action */
381 auto level_ = at_level();
382 if (!level_) {
383 //Respawn if player on a tile with no level and nowhere to go.
384 int tile_data = tile_data_at(m_tux->get_tile_pos());
385 if (!( tile_data & ( Tile::WORLDMAP_NORTH | Tile::WORLDMAP_SOUTH | Tile::WORLDMAP_WEST | Tile::WORLDMAP_EAST ))){
386 log_warning << "Player at illegal position " << m_tux->get_tile_pos().x << ", " << m_tux->get_tile_pos().y << " respawning." << std::endl;
387 move_to_spawnpoint("main");
388 return;
389 }
390 log_warning << "No level to enter at: " << m_tux->get_tile_pos().x << ", " << m_tux->get_tile_pos().y << std::endl;
391 return;
392 }
393
394 if (level_->get_pos() == m_tux->get_tile_pos()) {
395 try {
396 Vector shrinkpos = Vector(level_->get_pos().x * 32 + 16 - m_camera->get_offset().x,
397 level_->get_pos().y * 32 + 8 - m_camera->get_offset().y);
398 std::string levelfile = m_levels_path + level_->get_level_filename();
399
400 // update state and savegame
401 save_state();
402 ScreenManager::current()->push_screen(std::make_unique<GameSession>(levelfile, m_savegame, &level_->get_statistics()),
403 std::make_unique<ShrinkFade>(shrinkpos, 1.0f));
404 m_in_level = true;
405 } catch(std::exception& e) {
406 log_fatal << "Couldn't load level: " << e.what() << std::endl;
407 }
408 }
409 }
410 else
411 {
412 // tux->set_direction(input_direction);
413 }
414 }
415}
416
417int
418WorldMap::tile_data_at(const Vector& p) const
419{
420 int dirs = 0;
421
422 for (const auto& tilemap : get_solid_tilemaps()) {
423 const Tile& tile = tilemap->get_tile(static_cast<int>(p.x), static_cast<int>(p.y));
424 int dirdata = tile.get_data();
425 dirs |= dirdata;
426 }
427
428 return dirs;
429}
430
431int
432WorldMap::available_directions_at(const Vector& p) const
433{
434 return tile_data_at(p) & Tile::WORLDMAP_DIR_MASK;
435}
436
437LevelTile*
438WorldMap::at_level() const
439{
440 for (auto& level : get_objects_by_type<LevelTile>()) {
441 if (level.get_pos() == m_tux->get_tile_pos())
442 return &level;
443 }
444
445 return nullptr;
446}
447
448SpecialTile*
449WorldMap::at_special_tile() const
450{
451 for (auto& special_tile : get_objects_by_type<SpecialTile>()) {
452 if (special_tile.get_pos() == m_tux->get_tile_pos())
453 return &special_tile;
454 }
455
456 return nullptr;
457}
458
459SpriteChange*
460WorldMap::at_sprite_change(const Vector& pos) const
461{
462 for (auto& sprite_change : get_objects_by_type<SpriteChange>()) {
463 if (sprite_change.get_pos() == pos)
464 return &sprite_change;
465 }
466
467 return nullptr;
468}
469
470Teleporter*
471WorldMap::at_teleporter(const Vector& pos) const
472{
473 for (auto& teleporter : get_objects_by_type<Teleporter>()) {
474 if (teleporter.get_pos() == pos)
475 return &teleporter;
476 }
477
478 return nullptr;
479}
480
481void
482WorldMap::draw(DrawingContext& context)
483{
484 BIND_WORLDMAP(*this);
485
486 if (get_width() < static_cast<float>(context.get_width()) ||
487 get_height() < static_cast<float>(context.get_height()))
488 {
489 context.color().draw_filled_rect(Rectf(0, 0,
490 static_cast<float>(context.get_width()),
491 static_cast<float>(context.get_height())),
492 Color(0.0f, 0.0f, 0.0f, 1.0f), LAYER_BACKGROUND0);
493 }
494
495 context.push_transform();
496 context.set_translation(m_camera->get_offset());
497
498 GameObjectManager::draw(context);
499
500 if (g_debug.show_worldmap_path)
501 {
502 for (int x = 0; x < static_cast<int>(get_tiles_width()); x++) {
503 for (int y = 0; y < static_cast<int>(get_tiles_height()); y++) {
504 const int data = tile_data_at(Vector(static_cast<float>(x), static_cast<float>(y)));
505 const int px = x * 32;
506 const int py = y * 32;
507 const int W = 4;
508 const int layer = LAYER_FOREGROUND1 + 1000;
509 const Color color(1.0f, 0.0f, 1.0f, 0.5f);
510 if (data & Tile::WORLDMAP_NORTH) context.color().draw_filled_rect(Rect(px + 16-W, py , px + 16+W, py + 16-W), color, layer);
511 if (data & Tile::WORLDMAP_SOUTH) context.color().draw_filled_rect(Rect(px + 16-W, py + 16+W, px + 16+W, py + 32 ), color, layer);
512 if (data & Tile::WORLDMAP_EAST) context.color().draw_filled_rect(Rect(px + 16+W, py + 16-W, px + 32 , py + 16+W), color, layer);
513 if (data & Tile::WORLDMAP_WEST) context.color().draw_filled_rect(Rect(px , py + 16-W, px + 16-W, py + 16+W), color, layer);
514 if (data & Tile::WORLDMAP_DIR_MASK) context.color().draw_filled_rect(Rect(px + 16-W, py + 16-W, px + 16+W, py + 16+W), color, layer);
515 if (data & Tile::WORLDMAP_STOP) context.color().draw_filled_rect(Rect(px + 4 , py + 4 , px + 28 , py + 28 ), color, layer);
516 }
517 }
518 }
519
520 draw_status(context);
521 context.pop_transform();
522}
523
524void
525WorldMap::draw_status(DrawingContext& context)
526{
527 context.push_transform();
528 context.set_translation(Vector(0, 0));
529
530 if (!m_tux->is_moving()) {
531 for (auto& level : get_objects_by_type<LevelTile>()) {
532 if (level.get_pos() == m_tux->get_tile_pos()) {
533 context.color().draw_text(Resources::normal_font, level.get_title(),
534 Vector(static_cast<float>(context.get_width()) / 2.0f,
535 static_cast<float>(context.get_height()) - Resources::normal_font->get_height() - 10),
536 ALIGN_CENTER, LAYER_HUD, level.get_title_color());
537
538 if (g_config->developer_mode) {
539 context.color().draw_text(Resources::small_font, FileSystem::join(level.get_basedir(), level.get_level_filename()),
540 Vector(static_cast<float>(context.get_width()) / 2.0f,
541 static_cast<float>(context.get_height()) - Resources::normal_font->get_height() - 25),
542 ALIGN_CENTER, LAYER_HUD, level.get_title_color());
543 }
544
545 // if level is solved, draw level picture behind stats
546 /*
547 if (level.solved) {
548 if (const Surface* picture = level.get_picture()) {
549 Vector pos = Vector(context.get_width() - picture->get_width(), context.get_height() - picture->get_height());
550 context.push_transform();
551 context.set_alpha(0.5);
552 context.color().draw_surface(picture, pos, LAYER_FOREGROUND1-1);
553 context.pop_transform();
554 }
555 }
556 */
557 level.get_statistics().draw_worldmap_info(context, level.get_target_time());
558 break;
559 }
560 }
561
562 for (auto& special_tile : get_objects_by_type<SpecialTile>()) {
563 if (special_tile.get_pos() == m_tux->get_tile_pos()) {
564 /* Display an in-map message in the map, if any as been selected */
565 if (!special_tile.get_map_message().empty() && !special_tile.is_passive_message())
566 context.color().draw_text(Resources::normal_font, special_tile.get_map_message(),
567 Vector(static_cast<float>(context.get_width()) / 2.0f,
568 static_cast<float>(context.get_height()) - static_cast<float>(Resources::normal_font->get_height()) - 60.0f),
569 ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::message_color);
570 break;
571 }
572 }
573
574 // display teleporter messages
575 auto teleporter = at_teleporter(m_tux->get_tile_pos());
576 if (teleporter && (!teleporter->get_message().empty())) {
577 Vector pos = Vector(static_cast<float>(context.get_width()) / 2.0f,
578 static_cast<float>(context.get_height()) - Resources::normal_font->get_height() - 30.0f);
579 context.color().draw_text(Resources::normal_font, teleporter->get_message(), pos, ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::teleporter_message_color);
580 }
581 }
582
583 /* Display a passive message in the map, if needed */
584 if (m_passive_message_timer.started())
585 context.color().draw_text(Resources::normal_font, m_passive_message,
586 Vector(static_cast<float>(context.get_width()) / 2.0f,
587 static_cast<float>(context.get_height()) - Resources::normal_font->get_height() - 60.0f),
588 ALIGN_CENTER, LAYER_FOREGROUND1, WorldMap::message_color);
589
590 context.pop_transform();
591}
592
593void
594WorldMap::setup()
595{
596 auto& music_object = get_singleton_by_type<MusicObject>();
597 music_object.play_music(MusicType::LEVEL_MUSIC);
598
599 MenuManager::instance().clear_menu_stack();
600 ScreenManager::current()->set_screen_fade(std::make_unique<FadeToBlack>(FadeToBlack::FADEIN, 1.0f));
601
602 load_state();
603
604 // if force_spawnpoint was set, move Tux there, then clear force_spawnpoint
605 if (!m_force_spawnpoint.empty()) {
606 move_to_spawnpoint(m_force_spawnpoint, false, m_main_is_default);
607 m_force_spawnpoint = "";
608 m_main_is_default = true;
609 }
610
611 // If we specified a fade tilemap, let's fade it:
612 if (!m_initial_fade_tilemap.empty())
613 {
614 auto tilemap = get_object_by_name<TileMap>(m_initial_fade_tilemap);
615 if (tilemap != nullptr)
616 {
617 if (m_fade_direction == 0)
618 {
619 tilemap->fade(1.0, 1);
620 }
621 else
622 {
623 tilemap->fade(0.0, 1);
624 }
625 }
626 m_initial_fade_tilemap = "";
627 }
628
629 m_tux->setup();
630
631 // register worldmap_table as worldmap in scripting
632 m_squirrel_environment->expose_self();
633
634 //Run default.nut just before init script
635 try {
636 IFileStream in(m_levels_path + "default.nut");
637 m_squirrel_environment->run_script(in, "WorldMap::default.nut");
638 } catch(std::exception& ) {
639 // doesn't exist or erroneous; do nothing
640 }
641
642 if (!m_init_script.empty()) {
643 m_squirrel_environment->run_script(m_init_script, "WorldMap::init");
644 }
645 m_tux->process_special_tile( at_special_tile() );
646}
647
648void
649WorldMap::leave()
650{
651 // save state of world and player
652 save_state();
653
654 // remove worldmap_table from roottable
655 m_squirrel_environment->unexpose_self();
656
657 GameManager::current()->load_next_worldmap();
658}
659
660void
661WorldMap::set_levels_solved(bool solved, bool perfect)
662{
663 for (auto& level : get_objects_by_type<LevelTile>())
664 {
665 level.set_solved(solved);
666 level.set_perfect(perfect);
667 }
668}
669
670size_t
671WorldMap::level_count() const
672{
673 return get_object_count<LevelTile>();
674}
675
676size_t
677WorldMap::solved_level_count() const
678{
679 size_t count = 0;
680 for (auto& level : get_objects_by_type<LevelTile>()) {
681 if (level.is_solved()) {
682 count++;
683 }
684 }
685
686 return count;
687}
688
689void
690WorldMap::load_state()
691{
692 WorldMapState state(*this);
693 state.load_state();
694}
695
696void
697WorldMap::save_state()
698{
699 WorldMapState state(*this);
700 state.save_state();
701}
702
703void
704WorldMap::run_script(const std::string& script, const std::string& sourcename)
705{
706 m_squirrel_environment->run_script(script, sourcename);
707}
708
709void
710WorldMap::set_passive_message(const std::string& message, float time)
711{
712 m_passive_message = message;
713 m_passive_message_timer.start(time);
714}
715
716} // namespace worldmap
717
718/* EOF */
719